Classificação de acordes

Nossa proposta consiste em implementar uma ferramenta automática para identificação de tríades de formação de acordes, e consequentemente identificar os acordes que compõem gravações de instrumentos. Convencionalmente, tal identificação é feita através da escuta de trechos da gravação, por músicos treinados, que escrevem manualmente as cifras à medida que são identificadas.

A implementação consiste em uma análise do módulo do espectro de frequências da gravação em relação com o tempo, de modo a obter em intervalos de tempo fixos as notas predominantes e as tríades formadas por tais notas. As perguntas que motivam este trabalho são: qual grau de assertividade que uma análise nessas condições pode promover? Esse grau de assertividade auxiliaria um músico ou uma musicista a escrever cifras de determinada gravação?

Introdução

O espectro de frequências e fast Fourier transform

Em processamento digital de áudio, a análise no domínio da frequência, principalmente o de amplitude, pode trazer bastante informação sobre o sinal, mais do que a simples análise temporal.

In [3]:
Fs = 44100
Tmax = 2
A3 = 440.0
E3 = 329.63
t = np.linspace(0, Tmax, Tmax*Fs)
s = 0.7*np.sin(2*np.pi*A3*t) + 0.3*np.sin(2*np.pi*E3*t)
ipd.Audio(s, rate=Fs)
Out[3]:
In [4]:
plt.figure(figsize=(15,5))
plt.plot(t, s)
plt.xlabel('Tempo (s)')
plt.ylabel('Intensidade')
plt.xlim(0.0, 0.025)
Out[4]:
(0.0, 0.025)
In [5]:
S = fftpack.fft(s)
S = S/len(S)
f = np.linspace(-Fs/2, Fs/2, len(S))
Splot = np.abs(fftpack.fftshift(S))
plt.figure(figsize=(15,5))
plt.plot(f, Splot)
plt.xlabel('Frequência (Hz)')
plt.ylabel('Intensidade')
plt.xlim(300, 500)
Out[5]:
(300, 500)

Análise tempo-frequência e short-time Fourier transform

É possível dividir as amostras de um sinal de áudio em janelas de tempo, de modo que possamos determinar o espectro periódico do sinal.

In [6]:
tchirp = np.linspace(0, 3, 3*Fs)
s = signal.chirp(tchirp, 200.0, 3, 1000.0, 'logarithmic')
ipd.Audio(s, rate=Fs)
Out[6]:
In [7]:
f, t, Stf = signal.stft(s, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(0, 1000)
Out[7]:
(0, 1000)

Note que, por conta da FFT aplicada em uma janela de aproximadamente 50ms, há uma imprecisão na medida de frequência vista por uma linha espessa no gráfico. Ao aumentar o tamanho dessa janela, obtemos maior precisão da frequência, mas perdemos informação temporal.

In [8]:
tsig = np.linspace(0, 1, Fs)
tsil = np.linspace(0, 0.5, int(0.5*Fs))
# 2 segundos de 200Hz, silencio e 2 segundos de 300Hz
s1 = np.sin(2*np.pi*200.0*tsig)
silent = 1e-7*tsil
s2 = np.sin(2*np.pi*300.0*tsig)
s = np.concatenate((s1, silent, s2))

f, t, Stf = signal.stft(s, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(0, 600)
Out[8]:
(0, 600)

Frequências e escala cromática

A escala cromática contém 12 notas com intervalos de semitons. Como duas oitavas consecutivas possuem a relação de dobro/metade da frequência, define-se a relação de frequência entre semitons:

$$ {ST}_{n} = 2^{1/12} \cdot {ST}_{n-1} = 2^{n/12} \cdot {ST}_{0} $$

Sendo $${ST}_{0} = 55Hz$$ o Lá Zero (A0).

Podemos entender essa relação simétrica entre semitons desenhada por um círculo.

Figura 1 - Escala cromática vista como um círculo (Autor: David Eppstein)

Em termos de frequência, podemos associar as notas a pontos de uma curva helicoidal, cuja projeção destes pontos obtém o círculo da figura 1, e a distância entre pontos da mesma projeção (o passo da hélice) cresce o dobro em relação à anterior.

Figura 2 - Escala cromática vista como uma helicoidal

Vale ressaltar que muitos sistemas de afinações não usam esta escala simétrica de semitons.

Metodologia

Obtenção de notas do espectro de frequência

Passos seguidos:

  1. Relação de espectro (módulo ao quadrado) e tempo obtida pela STFT
  2. Para cada janela de tempo
    1. Determinar a frequência de centro pela equação $${ST}_{n} = 2^{n/12} \cdot {ST}_{0}$$
    2. Determinar os limites inferior e superior como $${ST}_{n} \pm 2.8\%$$ Baseado na distância média entre duas frequências de semitons
    3. Somar em uma lista de 12 notas os valores das amostras entre os limites
      1. n = 0, 12, 24, ... em A
      2. n = 1, 13, 25, ... em A#
      3. n = 2, 15, 26, ... em B
      4. ...
    4. Parar quando chegar em 5 kHz (ou metade da frequência de amostragem, segundo teorema de Nyquist-Shannon)
    5. Normalizar a lista dividindo pela soma
  3. O retorno é uma tabela cujas linhas são as notas e as colunas são as janelas de tempo em que se aplicou a STFT
In [10]:
Fs, data = wavfile.read('../wav/ChromaticScaleUp.wav')
ipd.Audio(data, rate=Fs)
Out[10]:
In [11]:
f, t, Stf = signal.stft(data, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(0, 2000)
Out[11]:
(0, 2000)
In [12]:
scale, t, Ch = chromagram_stft(data, rate=Fs)

plt.figure(figsize=(15,5))
chromaplot(t, scale, Ch)
plt.xlabel('Tempo (s)')
Out[12]:
Text(0.5, 0, 'Tempo (s)')

Obtenção de um acorde

Separamos as notas em tríades e decidimos o acorde maior, menor, diminuto ou aumentado

In [13]:
sample = 'C-A'

Fs, data = wavfile.read('../wav/{}.wav'.format(sample), 44100)
ipd.Audio(data, rate=Fs)
/home/caus/.local/lib/python3.6/site-packages/scipy/io/wavfile.py:273: WavFileWarning: Chunk (non-data) not understood, skipping it.
  WavFileWarning)
Out[13]:
In [14]:
f, t, Stf = signal.stft(data, fs=Fs, nperseg=2048)
plt.figure(figsize=(15,5))
Stfplot = np.abs(Stf)
plt.pcolormesh(t, f, Stfplot)
plt.ylabel('Frequência (Hz)')
plt.xlabel('Tempo (s)')
plt.ylim(20, 2000)
Out[14]:
(20, 2000)
In [15]:
scale, t, Ch = chromagram_stft(data, rate=Fs)

plt.figure(figsize=(15,5))
chromaplot(t, scale, Ch)
plt.xlabel('Tempo (s)')
Out[15]:
Text(0.5, 0, 'Tempo (s)')
In [16]:
note_groups = classifier.get_note_list(data, rate=Fs)

# Plotting results
plottable_groups = np.zeros(shape=(12, len(note_groups)))
scale = [s for s in scale]
for i in range(len(note_groups)):
    for note in note_groups[i]:
        plottable_groups[scale.index(note), i] = 1
        
plt.figure(figsize=(15,5))
chromaplot(t, scale, plottable_groups)
plt.xlabel('Tempo (s)')
Out[16]:
Text(0.5, 0, 'Tempo (s)')
In [18]:
plt.figure(figsize=(1,1))
chromaplot([0,1], ['Major', 'Minor', 'Diminished', 'Augmented', ''], [[4], [3], [2], [1], [0]])

plt.figure(figsize=(15,5))
chromaplot(np.linspace(t[0], t[-1], num=int(len(t)/window)), scale, plottable_chords)
plt.xlabel('Tempo (s)')
Out[18]:
Text(0.5, 0, 'Tempo (s)')
In [19]:
from precision_checker import print_precisions

print_precisions(sample)
The exact precision is 55.56%
The near precision is 55.56%

Testando com gravações

  1. Flamenco com um violão
In [20]:
sample = 'Reggae1'
Fs, data = wavfile.read('../wav/{}.wav'.format(sample), 44100)
scale, t, Ch = chromagram_stft(data, rate=Fs)
ipd.Audio(data, rate=Fs)
Out[20]:
In [21]:
note_groups = classifier.get_note_list(data, rate=Fs)

# Plotting results
plottable_groups = np.zeros(shape=(12, len(note_groups)))
scale = [s for s in scale]
for i in range(len(note_groups)):
    for note in note_groups[i]:
        plottable_groups[scale.index(note), i] = 1
        
plt.figure(figsize=(15,5))
chromaplot(t, scale, plottable_groups)
plt.xlabel('Tempo (s)')
Out[21]:
Text(0.5, 0, 'Tempo (s)')
In [23]:
plt.figure(figsize=(1,1))
chromaplot([0,1], ['Major', 'Minor', 'Diminished', 'Augmented', ''], [[4], [3], [2], [1], [0]])

plt.figure(figsize=(15,5))
chromaplot(np.linspace(t[0], t[-1], num=int(len(t)/window)), scale, plottable_chords)
plt.xlabel('Tempo (s)')
Out[23]:
Text(0.5, 0, 'Tempo (s)')
In [24]:
print_precisions(sample)
The exact precision is 49.32%
The near precision is 53.88%
  1. Rock (trilha de karaokê)
In [25]:
sample = 'Rock1'
Fs, data = wavfile.read('../wav/{}.wav'.format(sample), 44100)
scale, t, Ch = chromagram_stft(data, rate=Fs)
ipd.Audio(data, rate=Fs)
Out[25]:
In [26]:
note_groups = classifier.get_note_list(data, rate=Fs)

# Plotting results
plottable_groups = np.zeros(shape=(12, len(note_groups)))
scale = [s for s in scale]
for i in range(len(note_groups)):
    for note in note_groups[i]:
        plottable_groups[scale.index(note), i] = 1
        
plt.figure(figsize=(15,5))
chromaplot(t, scale, plottable_groups)
plt.xlabel('Tempo (s)')
Out[26]:
Text(0.5, 0, 'Tempo (s)')
In [28]:
plt.figure(figsize=(1,1))
chromaplot([0,1], ['Major', 'Minor', 'Diminished', 'Augmented', ''], [[4], [3], [2], [1], [0]])

plt.figure(figsize=(15,5))
chromaplot(np.linspace(t[0], t[-1], num=int(len(t)/window)), scale, plottable_chords)
plt.xlabel('Tempo (s)')
Out[28]:
Text(0.5, 0, 'Tempo (s)')
In [29]:
print_precisions(sample)
The exact precision is 30.36%
The near precision is 31.55%